package core;

import java.util.Observable;
import java.util.Observer;

import persistence.ContainerDTO;
import persistence.DataPersistor;

import core.exception.IllegalAddException;
import core.exception.IllegalEditException;
import core.exception.IllegalRemoveException;
import core.exception.NotFoundException;
import core.exception.SaveException;

/**
 * ContainerFacade is the model facade for all operations on Containers.  This class is a 
 * Singleton and is Observable.
 * @author Royce
 *
 */
public class ContainerFacade extends Observable {

	private static ContainerFacade _instance;
	private static Observer _controller;
	
	/**
	 * A method to access the singleton Instance of this ContainerFacade
	 * @return the instance, creating it if necessary.
	 * 
	 * @pre none.
	 * @post returns a valid ContainerFacade.
	 */
	public static ContainerFacade instance() {
		if (_instance == null) {
			_instance = new ContainerFacade();
		}
		return _instance;
	}
	
	public void addControllerForTest(Observer observer){
		_controller = observer;
	}
	
	/**
	 * Sets the instance of this ContainerFacade to the one given
	 * @param facade the ContainerFacade instance to replace this one
	 */
	protected static void setInstance(ContainerFacade facade) {
		_instance = facade;
	}
	
	/** This method is for testing purposes to reset the container facade and
	 * everything it depends on.
	 */
	public static void clearAllData(){
		_instance = null;
		_controller = null;
		RootStorageUnit.setInstance(null);
	}
	
	/**
	 * Private constructor.  This method should never be called outside 
	 * the ContainerFacade class.
	 */
	private ContainerFacade() {
		//just make sure the instance exists
		RootStorageUnit.instance();
	}
	
	
	/**
	 * Determines if it is valid to rename a given Container.
	 * @param container The Container to rename.
	 * @param newName The new name for the Container.
	 * @return true if it is valid to rename this Container, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement
	 */
	public boolean canRenameContainer(Container container, String newName) {		
		return container.getParent().canSetChildContainerName(container.getName(), newName);
	}
	
	/**
	 * Renames the given Container.
	 * @param container The container to rename.
	 * @param newName The new name for the Container.
	 * @throws IllegalEditException if it is invalid to rename this Container.
	 * @throws NotFoundException if the given Container is not found.
	 * @throws SaveException if can't save
	 * 
	 * @pre container exist, and no container with the name newName exists
	 * @post container is now named newName
	 */
	public void renameContainer(Container container, String newName) 
			throws IllegalEditException, NotFoundException, SaveException {
		// Perform action
		container.getParent().setChildContainerName(container.getName(), newName);
		
		// Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ContainerDTO dto = new ContainerDTO(container);
		DataPersistor.instance().getContainerDAO().updateContainer(dto);		
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		// Notify
		Notification notify = new Notification(container, NotificationType.ContainerChanged);
		notifyObservers(notify);
	}
	
	/**
	 * Determines if it is valid to add a new StorageUnit
	 * @param name The name for the StorageUnit
	 * @return true if it is valid to add a StorageUnit with the given name, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement
	 */
	public boolean canAddStorageUnit(String name) {
		return (!(RootStorageUnit.instance().getName().equals(name)) &&
				(RootStorageUnit.instance().canAddContainer(name)));
	}
	
	/**
	 * Adds a StorageUnit with the given name.
	 * @param name A name for the StorageUnit
	 * @throws IllegalAddException if it is invalid to add a StorageUnit with the given name.
	 * @return a reference to the added Container, if successful
	 * @throws SaveException If unable to save
	 * 
	 * @pre no StorageUnit with the given name exists.
	 * @post a StorageUnit is created as a child of RootStorageUnit with the given name
	 */
	public Container addStorageUnit(String name) throws IllegalAddException, SaveException {
		// Perform Action
		Container addedContainer = addStorageUnitPrivate(name);
		
		// Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ContainerDTO dto = new ContainerDTO(addedContainer);
		DataPersistor.instance().getContainerDAO().addContainer(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		// Notify
		Notification notify = new Notification(addedContainer, NotificationType.ContainerAdded);
		notifyObservers(notify);
		return addedContainer;
	}
	
	/**
	 * Determines if it is valid to add a ProductGroup to a given Container.
	 * @param parentContainer The Container to add a ProductGroup to.
	 * @param name The name of the new ProductGroup.
	 * @return true if it is valid to do so, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement
	 */
	public boolean canAddProductGroup(Container parentContainer, String name) {
		return parentContainer.canAddContainer(name);
	}
	
	/**
	 * Adds a new ProductGroup to the given Container.
	 * @param parentContainer The Container to add the ProductGroup to.
	 * @param name The name of the new ProductGroup.
	 * @throws IllegalAddException if it is invalid to add a ProductGroup with the given 
	 * name into the given Container
	 * @throws NotFoundException if the given Container is not found in the system.
	 * @return a reference to the added ProductGroup, if sucessful
	 * @throws SaveException if can't save
	 * 
	 * @pre parentContainer exists and is not a RootStorageUnit, no ProductGroup with the
	 * given name is in the given parentContainer
	 * @post parentContainer now contains an empty ProductGroup with the given name.
	 */
	public Container addProductGroup(Container parentContainer, String name, 
			String threeMonthQuantity, String threeMonthUnit) 
			throws IllegalAddException, NotFoundException, SaveException {
		
		// Perform Action
		if (!canAddProductGroup(parentContainer,name) || !validThreeMonthSupply(threeMonthQuantity,
				threeMonthUnit))
			throw new IllegalAddException("Bad add product group");
		
		Container pg = addProductGroupPrivate(parentContainer,name,threeMonthQuantity,
				threeMonthUnit);
		
		// Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ContainerDTO dto = new ContainerDTO(pg);
		DataPersistor.instance().getContainerDAO().addContainer(dto);		
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		// Notify
		Notification notify = new Notification(pg, NotificationType.ContainerAdded);
		notifyObservers(notify);
		return pg;
	}
	
	/**
	 * Determines if it is valid to remove a given Container from the system.
	 * @param container The Container to test for removing.
	 * @return true if it is valid to remove the given Container
	 * 
	 * @pre none
	 * @post see return statement
	 */
	public boolean canRemoveContainer(Container container) {
		if (container == null) {
			return false;
		}
		if (container instanceof RootStorageUnit) {
			return false;
		}
		return container.getParent().canRemoveContainer(container.getName());
	}
	
	/**
	 * Removes the given Container from the system.
	 * @param container the Container to remove.
	 * @throws IllegalRemoveException if it is invalid to remove the given Container.
	 * @throws NotFoundException if the given Container is not found in the system.
	 * @throws SaveException if unable to save
	 * 
	 * @pre the container exists in the system, is empty, and is not the RootStorageUnit
	 * @post the container no longer exists in the system.
	 */
	public void removeContainer(Container container) 
			throws IllegalRemoveException, NotFoundException, SaveException {
		
		// Perform Action
		if (container == null) {
			throw new IllegalRemoveException("You can't remove a null container!");
		}
		
		if (container instanceof RootStorageUnit) {
			throw new IllegalRemoveException("You can't remove the root!");
		}
		container.getParent().removeContainer(container.getName());
		
		// Note: Remove in container saves, because multiple containers may be
		// deleted on a delete, and there isn't a nice way to ask for them.
		
		// Notify
		Notification notify = new Notification(container, NotificationType.ContainerRemoved);
		notifyObservers(notify);
	}

	/**
	 * Test if the three month supply is valid for the container. For an amount to be
	 * valid, the Container must be a ProductGroup, amount must be nonnegative, and if
	 * amount is count, it the quantity must be an integer.
	 * @param container
	 * @param quantity
	 * @param unit
	 * @return
	 */
	public boolean canSetThreeMonthSupply(Container container, String quantity, String unit){
		return container instanceof ProductGroup && validThreeMonthSupply(quantity,unit);
	}

	
	/**
	 * Set the three month supply for the given container to the given amount. This
	 * will throw an exception if canSetThreeMonthSupply would return false.
	 * @param container The Container to change.
	 * @param quantity The quantity of the amount
	 * @param unit The unit of the amount
	 * @throws IllegalArgumentException
	 * @throws SaveException if can't save
	 */
	public void setThreeMonthSupply(Container container, String quantity, String unit) 
			throws IllegalArgumentException, SaveException {
		
		// Perform Action
		if (!canSetThreeMonthSupply(container, quantity,unit))
			throw new IllegalArgumentException("Unable to set 3 month supply");
		
		Amount amount = new Amount(quantity,unit); // if fails, will pass along IllegalArgException
		((ProductGroup)container).setThreeMonthSupply(amount);
		
		// Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ContainerDTO dto = new ContainerDTO(container);
		DataPersistor.instance().getContainerDAO().updateContainer(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		// Notify
		Notification notify = new Notification(container, NotificationType.ContainerChanged);
		notifyObservers(notify);
		
	}
	
	/**
	 * Test if a particular amount is valid for a three month supply for a product
	 * group. (3 month supplies do not depend on the product group, only on the amount).
	 * @param quantity the quantity of the amount to test
	 * @param unit the unit of the amount to test.
	 * @return
	 */
	public boolean validThreeMonthSupply(String quantity, String unit){
		try{
			Amount amount = new Amount(quantity,unit);
			return ProductGroup.canSetThreeMonthSupply(amount);
		}
		catch (IllegalArgumentException e){
			return false;
		}
		
	}
	
	/** Return the quantity needed for a three month supply, or empty
	 * string if the container is not a Product Group
	 * @param c the container to get the three month supply for
	 * @return the string quantity, or empty string if not applicable
	 */
	public String get3MonthSupplyQuantity(Container c){
		if (! (c instanceof ProductGroup))
			return "";
		Amount a = ((ProductGroup)c).getThreeMonthSupply();
		return Float.toString(a.getQuantity());
	}
	
	
	/** Return the unit as a string for the containers three month
	 * supply or empty string if the container is not a product group.
	 * 
	 * @param c the container to get the three month supply for
	 * @return the string unit, or empty string if not applicable	 
	 */
	public String get3MonthSupplyUnit(Container c){
		if (! (c instanceof ProductGroup))
			return "";
		Amount a = ((ProductGroup)c).getThreeMonthSupply();
		return a.getUnit().toString();	
	}
	
	/**
	 * 
	 * @return a reference to the root Container.
	 */
	public Container get_rootUnit() {
		return RootStorageUnit.instance();
	}

	@Override
	public void notifyObservers(Object arg) {
		if (_controller == null){
			this.setChanged();
			super.notifyObservers(arg);
		}
		else
			_controller.update(this, arg);
	}
	
	/**
	 * This function is intended for use by the loaders. It is like addStorageUnit
	 * except it does not notify observers or attempt to save.
	 * @param name
	 * @return the newly created storage unit
	 * @throws IllegalAddException 
	 */
	public Container createStorageUnit(String name) throws IllegalAddException{
		return addStorageUnitPrivate(name);
	}
	
	/**
	 * This function is intended for use by the loaders. It is like addProductGroup 
	 * except it does not notify observers or attempt to save.
	 * @param parent the parent container
	 * @param name the name of the new container
	 * @param threeMonthSupply the three month supply value
	 * @param threeMonthUnit the three month supply unit
	 * @return the created product group
	 * @throws IllegalAddException
	 * @throws NotFoundException
	 */
	public Container createProductGroup(Container parent, String name, String threeMonthSupply,
			 String threeMonthUnit) throws IllegalAddException, NotFoundException{
		return addProductGroupPrivate(parent,name,threeMonthSupply,
				threeMonthUnit);
	}
	private Container addStorageUnitPrivate(String name) throws IllegalAddException{
		return 	RootStorageUnit.instance().addContainer(name);
	}
	
	private Container addProductGroupPrivate(Container parentContainer, String name, 
			String threeMonthQuantity, String threeMonthUnit) 
			throws IllegalAddException, NotFoundException {
		
		Amount a = new Amount(threeMonthQuantity, threeMonthUnit);
		Container pg = parentContainer.addContainer(name);
		((ProductGroup)pg).setThreeMonthSupply(a);
		return pg;

	}	
	
	
	
}
